package typoker.logic;

import java.util.Arrays;
import java.util.Collections;
import java.util.Random;
import java.util.Set;
import java.util.TreeSet;

import typoker.GameProxy;
import typoker.utils.Util;

public class Game implements Runnable {
	private static final int smallBlind = 10;
	private static final int bigBlind = 2 * smallBlind;
	private static final int nrOfPlayers = 9;

	private static final String[] names = Util.readStringFromFile("names.dat");
	private static final String[] images = Util
			.readStringFromFile("images.dat");

	private static Random random = new Random();
	private final Chat chat;
	private boolean gameOver;

	private int round = 0;
	private int stage = 0;

	private final int human;
	private boolean humanTurn = false;

	private int playersInGame = nrOfPlayers;
	private int playersInRound = nrOfPlayers;
	private int playersBetting = nrOfPlayers;

	private int dealer;
	private int position = nrOfPlayers + 1;
	private int raiseMarker = nrOfPlayers + 1;

	private int pot = 0;
	private int maxBet = 0;
	private int currentBet = 0;
	private int step = 0;

	private int[] winners = null;

	private Player[] players = new Player[nrOfPlayers];
	private Card[] commCards = new Card[5];
	private Deck deck = null;

	public Game(String name, String image) {
		human = random.nextInt(nrOfPlayers);

		String[] temp = names.clone();
		Collections.shuffle(Arrays.asList(temp));

		for (int i = 0; i < nrOfPlayers; i++) {
			if (i == human) {
				players[i] = new Player(name, image);
			} else {
				if (temp[i] == name) {
					players[i] = new Player(temp[names.length - 1],
							images[random.nextInt(images.length)]);
				} else {
					players[i] = new Player(temp[i],
							images[random.nextInt(images.length)]);
				}
			}
		}

		chat = new Chat(players, human);

		dealer = random.nextInt(nrOfPlayers);

		round = 0;
		gameOver = false;

		System.out.println(name + " joins the game");
	}

	public void run() {
		System.out.println("Start of Game");

		while (!gameOver && playersInGame > 1) {
			sleep(1000);
			startRound();
			advanceStage();
			if (!gameOver) {
				distributeWinnings();
				sleep(2000);
				endRound();
			}
		}

		System.out.println("End of Game");
	}

	public synchronized void killThread() {
		gameOver = true;
		chat.stop();

		if (humanTurn) {
			notify();
		}

		System.out.println(players[human].getName() + " leaves the game");
	}

	private void sleep(int n) {
		try {
			Thread.sleep(n);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	private int nextPosition(int p) {
		p++;
		if (p >= nrOfPlayers) {
			p %= nrOfPlayers;
		}

		return p;
	}

	private int nextPlayer(int prev) {
		int p = nextPosition(prev);

		while (!players[p].isBetting() && p != prev) {
			p = nextPosition(p);
		}

		return p;
	}

	private void nextDealer() {
		int p = nextPosition(dealer);

		while (!players[p].isInGame()) {
			p = nextPosition(p);
		}

		GameProxy.updateDealer(dealer, p);
		dealer = p;
	}

	private void startRound() {
		round++;
		humanTurn = false;
		winners = null;

		deck = Deck.getShuffledDeck();

		playersInRound = playersInGame;
		playersBetting = playersInGame;

		step = bigBlind;
		currentBet = 0;
		maxBet = 0;
		pot = 0;

		stage = 0;

		GameProxy.setRaise(step);

		System.out.println("Start of Round " + round);
		System.out.println("Players left: " + playersInGame);
		System.out.println(players[dealer].getName() + " is the Dealer");
	}

	private void distributeWinnings() {
		winners = new int[playersInRound];
		int[] win = new int[playersInRound];
		int[] lose = new int[nrOfPlayers - playersInRound];

		int w = 0;
		int l = 0;

		for (int i = 0; i < nrOfPlayers; i++) {
			if (players[i].isInRound()) {
				winners[w] = i;
				win[w] = players[i].getBet();
				w++;
			} else {
				lose[l++] = players[i].getBet();
			}
		}

		while (w > 0) {
			int minBet = Math.min(findMinBet(win), findMinBet(lose));
			int winnings = calculatePot(minBet, win)
					+ calculatePot(minBet, lose);
			winnings /= w;

			for (int i = 0; i < playersInRound; i++) {
				if (win[i] > 0) {
					players[winners[i]].collectWinnings(winnings);
					win[i] -= minBet;
					if (win[i] == 0) {
						w--;
					}
				}
			}

			for (int i = 0; i < lose.length; i++) {
				lose[i] -= minBet;
			}
		}

		l = 0;
		for (int i = 0; i < nrOfPlayers; i++) {
			if (!players[i].isInRound()) {
				if (lose[l] > 0) {
					players[i].collectWinnings(lose[l]);
				}
				l++;
			}
		}

		GameProxy.enableWinners(winners);
	}

	private static int findMinBet(int[] a) {
		int min = Integer.MAX_VALUE;

		for (int x : a) {
			if (x > 0 && x < min) {
				min = x;
			}
		}

		return min;
	}

	private static int calculatePot(int bet, int[] a) {
		int n = 0;

		for (int x : a) {
			if (x >= bet) {
				n++;
			}
		}

		return bet * n;
	}

	private void endRound() {
		pot = 0;

		GameProxy.endRound();

		for (int i = 0; i < nrOfPlayers; i++) {
			if (players[i].isInRound()) {
				System.out.println(players[i].getName() + " won "
						+ players[i].getWinnings() + "$");
			}

			if (players[i].isInGame() && players[i].getCash() == 0) {
				players[i].leaveGame();
				playersInGame--;
				System.out.println(players[i].getName() + " leaves the game");
			} else if (players[i].isInGame()) {
				System.out.println(players[i].getName() + " has "
						+ players[i].getCash() + "$");
			}

			players[i].reset();
			GameProxy.updatePlayer(i, players[i]);
		}

		commCards = new Card[5];

		System.out.println("End of Round " + round);

		if (playersInGame > 1) {
			nextDealer();
		}
	}

	private void advanceStage() {
		sleep(1000);
		System.out.print("Advance to ");

		switch (stage) {
		case 0:
			System.out.println("pre-flop");
			dealCards();

			step = bigBlind;
			maxBet = 4 * step;
			position = placeBlinds(dealer);
			currentBet = bigBlind;
			raiseMarker = nrOfPlayers + 1;

			betting();
			break;
		case 1:
			System.out.println("flop");
			burnCard();

			dealCommCard(0);
			dealCommCard(1);
			dealCommCard(2);

			maxBet = currentBet + 4 * step;
			position = nextPlayer(dealer);
			raiseMarker = nrOfPlayers + 1;

			betting();
			break;
		case 2:
			System.out.println("turn");
			burnCard();

			dealCommCard(3);

			step = 2 * bigBlind;
			GameProxy.setRaise(step);
			maxBet = currentBet + 4 * step;
			position = nextPlayer(dealer);
			raiseMarker = nrOfPlayers + 1;

			betting();
			break;
		case 3:
			System.out.println("river");
			burnCard();

			dealCommCard(4);

			maxBet = currentBet + 4 * step;
			position = nextPlayer(dealer);
			raiseMarker = nrOfPlayers + 1;

			betting();
			break;
		case 4:
			System.out.println("showdown");
			position = nrOfPlayers + 1;
			showdown();
			return;
		default:
			System.err.println("Problem with gamestages");
		}
	}

	private int placeBlinds(int p) {
		p = nextPosition(p);
		while (!players[p].isInGame()) {
			p = nextPosition(p);
		}
		if (players[p].getCash() > smallBlind) {
			players[p].makeBet(smallBlind);
		} else {
			players[p].allin();
			playersBetting--;
		}
		pot = players[p].getBet();
		System.out.println(players[p].getName() + " places "
				+ players[p].getBet() + "$ as Small Blind");
		GameProxy.updatePlayer(p, players[p]);

		p = nextPosition(p);
		while (!players[p].isInGame()) {
			p = nextPosition(p);
		}
		if (players[p].getCash() > bigBlind) {
			players[p].makeBet(bigBlind);
		} else {
			players[p].allin();
			playersBetting--;
		}
		pot += players[p].getBet();
		System.out.println(players[p].getName() + " places "
				+ players[p].getBet() + "$ as Big Blind");
		GameProxy.updatePlayer(p, players[p]);

		System.out.println("Current pot: " + pot + "$");
		GameProxy.setPot(pot);

		return nextPlayer(p);
	}

	private void dealCards() {
		System.out.println("Dealing cards");
		int p = nextPosition(dealer);
		Card card = null;
		String image = null;

		for (int i = 0; i < 2; i++) {
			for (int j = 0; j < nrOfPlayers; j++, p = nextPosition(p)) {
				if (gameOver) {
					return;
				}

				if (players[p].isInGame()) {
					card = deck.getCard();
					players[p].addCard(card);

					if (p == human) {
						image = card.getImage();
					} else {
						image = "B.png";
					}
					GameProxy.dealCard(p, image, i);

					sleep(300);
				}
			}
		}

		players[human].showHand();

		for (Player player : players) {
			if (player.isInGame()) {
				Card[] hand = player.getHand();
				System.out.println(player.getName() + ": " + hand[0] + ", "
						+ hand[1]);
			}
		}
	}

	private void burnCard() {
		deck.getCard();
		System.out.println("Dealer places one card face down");
		GameProxy.burnCard();
		sleep(500);
	}

	private void dealCommCard(int nr) {
		commCards[nr] = deck.getCard();
		System.out.println((nr + 1) + ". community card: " + commCards[nr]);
		GameProxy.dealCommCard(commCards[nr].getImage(), nr);
		sleep(500);
	}

	private void betting() {
		GameProxy.enableActive(position);

		while (!gameOver && stage < 4 && playersInRound > 1) {
			if (playersBetting <= 0 || position == raiseMarker) {
				GameProxy.disableActive();
				stage++;
				advanceStage();
				return;
			}

			if (raiseMarker > nrOfPlayers) {
				raiseMarker = position;
			}

			if (position != human) {
				sleep(1000);
				randomMove(getOptions(position));
			} else {
				synchronized (this) {
					humanTurn = true;
					GameProxy.enableButtons(getOptions(position));

					try {
						wait();
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			}

			position = nextPlayer(position);
			GameProxy.updateActive(position);
		}

		GameProxy.disableActive();
	}

	private void showdown() {
		playersBetting = 0;
		int bestHand = 9999;

		for (int i = 0; i < nrOfPlayers; i++) {
			if (players[i].isInRound()) {
				players[i].showHand();
				GameProxy.showHand(i, players[i].getCards());

				int handValue = evaluateHand(i);

				if (handValue < bestHand) {
					bestHand = handValue;

					for (int j = 0; j < i; j++) {
						if (players[j].isInRound()) {
							players[j].loseRound();
							playersInRound--;
						}
					}
				} else if (handValue > bestHand) {
					players[i].loseRound();
					playersInRound--;
				}
			}
		}
	}

	private int evaluateHand(int p) {
		int best = 9999;

		Card[] cards = new Card[2 + commCards.length];

		Card[] temp = players[p].getHand();
		cards[0] = temp[0];
		cards[1] = temp[1];

		for (int i = 0; i < commCards.length; i++) {
			cards[2 + i] = commCards[i];
		}

		for (int i = 0; i < cards.length - 4; i++) {
			for (int j = i + 1; j < cards.length - 3; j++) {
				for (int k = j + 1; k < cards.length - 2; k++) {
					for (int l = k + 1; l < cards.length - 1; l++) {
						for (int m = l + 1; m < cards.length; m++) {
							int value = Evaluator.evaluate(cards[i], cards[j],
									cards[k], cards[l], cards[m]);

							if (value < best) {
								best = value;
							}
						}
					}
				}
			}
		}

		return best;
	}

	private int[] getOptions(int p) {
		Set<Integer> options = new TreeSet<Integer>();

		options.add(1);

		if (players[p].getBet() == currentBet) {
			options.add(2);
		} else if (players[p].getBet() < currentBet
				&& currentBet < players[p].getAssets()) {
			options.add(3);
		} else {
			options.add(5);
		}

		if (currentBet < maxBet && playersBetting > 1) {
			if (currentBet + step < players[p].getAssets()) {
				options.add(4);
			} else {
				options.add(5);
			}
		}

		if (playersBetting <= 1) {
			playersBetting = 0;
		}

		return toIntArray(options.toArray(new Integer[1]));
	}

	private static int[] toIntArray(Integer[] a) {
		int[] b = new int[a.length];

		for (int i = 0; i < a.length; i++) {
			b[i] = a[i];
		}

		return b;
	}

	private void randomMove(int[] options) {
		makeMove(options[random.nextInt(options.length)]);
	}

	public synchronized void humanMove(int move) {
		if (humanTurn) {
			if (makeMove(move)) {
				GameProxy.disableButtons();
				humanTurn = false;
				notify();
			}
		}
	}

	private boolean isValidMove(int move) {
		int[] options = getOptions(position);

		for (int option : options) {
			if (move == option) {
				return true;
			}
		}

		return false;
	}

	private boolean makeMove(int move) {
		if (isValidMove(move)) {
			switch (move) {
			case 1:
				fold();
				break;
			case 2:
				check();
				break;
			case 3:
				call();
				break;
			case 4:
				raise();
				break;
			case 5:
				allin();
				break;
			default:
				System.err.println("Problem with move");
				return false;
			}

			return true;
		}

		return false;
	}

	private void fold() {
		players[position].fold();
		playersBetting--;
		playersInRound--;

		System.out.println(players[position].getName() + " folds");
		GameProxy.foldCards(position);
	}

	private void check() {
		System.out.println(players[position].getName() + " checks");
	}

	private void call() {
		int temp = players[position].getBet();
		players[position].makeBet(currentBet);
		pot += players[position].getBet() - temp;

		System.out.println(players[position].getName() + " calls to "
				+ players[position].getBet() + "$");
		System.out.println("Current pot: " + pot + "$");
		GameProxy.updatePlayer(position, players[position]);
		GameProxy.setPot(pot);
	}

	private void raise() {
		int temp = players[position].getBet();
		players[position].makeBet(currentBet + step);
		currentBet = players[position].getBet();
		pot += currentBet - temp;
		raiseMarker = position;

		System.out.println(players[position].getName() + " raises to "
				+ players[position].getBet() + "$");
		System.out.println("Current pot: " + pot + "$");
		GameProxy.updatePlayer(position, players[position]);
		GameProxy.setPot(pot);
	}

	private void allin() {
		int temp = players[position].getBet();
		players[position].allin();
		pot += players[position].getBet() - temp;

		if (players[position].getBet() > currentBet) {
			currentBet = players[position].getBet();
			raiseMarker = position;
		}

		playersBetting--;

		System.out.println(players[position].getName() + " goes all-in to "
				+ players[position].getBet() + "$");
		System.out.println("Current pot: " + pot + "$");
		GameProxy.updatePlayer(position, players[position]);
		GameProxy.setPot(pot);
	}

	public Chat getChat() {
		return chat;
	}

	public static int getSmallBlind() {
		return smallBlind;
	}

	public static int getBigBlind() {
		return bigBlind;
	}

	public int getHuman() {
		return human;
	}

	public int getDealer() {
		return dealer;
	}

	public int getPosition() {
		return position;
	}

	public int getPot() {
		return pot;
	}

	public int getStep() {
		return step;
	}

	public Player[] getPlayers() {
		return players;
	}

	public Card[] getCommCards() {
		return commCards;
	}

	public int[] getButtons() {
		if (humanTurn) {
			return getOptions(human);
		}

		return null;
	}
}